package com.groupYLP.CouponNexus.merchant.admin.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.groupYLP.CouponNexus.framework.exception.ClientException;
import com.groupYLP.CouponNexus.framework.exception.ServiceException;
import com.groupYLP.CouponNexus.merchant.admin.common.constant.MerchantAdminRedisConstant;
import com.groupYLP.CouponNexus.merchant.admin.common.context.UserContext;
import com.groupYLP.CouponNexus.merchant.admin.common.enums.ChainBizMarkEnum;
import com.groupYLP.CouponNexus.merchant.admin.common.enums.CouponTemplateStatusEnum;
import com.groupYLP.CouponNexus.merchant.admin.dao.entity.CouponTemplateDO;
import com.groupYLP.CouponNexus.merchant.admin.dao.mapper.CouponTemplateMapper;
import com.groupYLP.CouponNexus.merchant.admin.dto.req.CouponTemplateNumberReqDTO;
import com.groupYLP.CouponNexus.merchant.admin.dto.req.CouponTemplatePageQueryReqDTO;
import com.groupYLP.CouponNexus.merchant.admin.dto.req.CouponTemplateSaveReqDTO;
import com.groupYLP.CouponNexus.merchant.admin.dto.res.CouponTemplatePageQueryRespDTO;
import com.groupYLP.CouponNexus.merchant.admin.dto.res.CouponTemplateQueryRespDTO;
import com.groupYLP.CouponNexus.merchant.admin.mq.event.CouponTemplateDelayEvent;
import com.groupYLP.CouponNexus.merchant.admin.mq.producer.CouponTemplateDelayExecuteStatusProducer;
import com.groupYLP.CouponNexus.merchant.admin.service.CouponTemplateService;
import com.groupYLP.CouponNexus.merchant.admin.service.basics.chain.MerchantAdminChainContext;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.starter.annotation.LogRecord;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class CouponTemplateServiceImpl extends ServiceImpl<CouponTemplateMapper, CouponTemplateDO> implements CouponTemplateService {

    private final CouponTemplateMapper couponTemplateMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final MerchantAdminChainContext merchantAdminChainContext;
    private final CouponTemplateDelayExecuteStatusProducer couponTemplateDelayExecuteStatusProducer;
    private final RBloomFilter<String> couponTemplateQueryBloomFilter;


    @SneakyThrows
    @LogRecord(
            success = """
                    创建优惠券：{{#requestParam.name}}， \
                    优惠对象：{COMMON_ENUM_PARSE{'DiscountTargetEnum' + '_' + #requestParam.target}}， \
                    优惠类型：{COMMON_ENUM_PARSE{'DiscountTypeEnum' + '_' + #requestParam.type}}， \
                    库存数量：{{#requestParam.stock}}， \
                    优惠商品编码：{{#requestParam.goods}}， \
                    有效期开始时间：{{#requestParam.validStartTime}}， \
                    有效期结束时间：{{#requestParam.validEndTime}}， \
                    领取规则：{{#requestParam.receiveRule}}， \
                    消耗规则：{{#requestParam.consumeRule}};
                    """,
            type = "CouponTemplate",
            bizNo = "{{#bizNo}}",
            extra = "{{#requestParam.toString()}}"
    )
    @Override
    public void createCouponTemplate(CouponTemplateSaveReqDTO requestParam) {
        //通过责任链验证请求参数是否正确
        merchantAdminChainContext.handler(ChainBizMarkEnum.MERCHANT_ADMIN_CREATE_COUPON_TEMPLATE_KEY.name(), requestParam);

        // 新增优惠券模板信息到数据库
        CouponTemplateDO couponTemplateDO = BeanUtil.toBean(requestParam, CouponTemplateDO.class);
        couponTemplateDO.setStatus(CouponTemplateStatusEnum.ACTIVE.getStatus());
        couponTemplateDO.setShopNumber(UserContext.getShopNumber());
        couponTemplateMapper.insert(couponTemplateDO);

        // 因为模板 ID 是运行中生成的，@LogRecord 默认拿不到，所以我们需要手动设置
        LogRecordContext.putVariable("bizNo", couponTemplateDO.getId());

        // 缓存预热：通过将数据库的记录序列化成 JSON 字符串放入 Redis 缓存
        //使用BeanUtil将CouponTemplateDO对象转换为CouponTemplateQueryRespDTO对象。
        //BeanUtil通过反射机制查找源对象和目标对象中具有相同名称的属性，并进行值的复制
        CouponTemplateQueryRespDTO actualRespDTO = BeanUtil.toBean(couponTemplateDO, CouponTemplateQueryRespDTO.class);

        //将转换后的CouponTemplateQueryRespDTO对象转换为Map集合。
        //BeanUtil同样通过反射机制获取对象的所有属性及其值，并将其放入Map中。
        Map<String, Object> cacheTargetMap = BeanUtil.beanToMap(actualRespDTO, false, true);

        //处理Map中的值，确保所有值都转换为String类型。
        //这样可以保证存入Redis时数据类型的统一性。
        Map<String, String> actualCacheTargetMap = cacheTargetMap.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        entry -> entry.getValue() != null ? entry.getValue().toString() : ""
                ));

        //生成Redis缓存键名，根据优惠券模板ID。
        String couponTemplateCacheKey = String.format(MerchantAdminRedisConstant.COUPON_TEMPLATE_KEY, couponTemplateDO.getId());

        //将处理后的Map数据存入Redis的Hash结构中，实现数据缓存。
        stringRedisTemplate.opsForHash().putAll(couponTemplateCacheKey, actualCacheTargetMap);

        // 通过 LUA 脚本执行设置 Hash 数据以及设置过期时间
        // LUA 脚本可以保证操作的原子性，避免在设置 Hash 数据和设置过期时间之间出现数据不一致的情况
        String luaScript = "redis.call('HMSET', KEYS[1], unpack(ARGV, 1, #ARGV - 1)) " +
                "redis.call('EXPIREAT', KEYS[1], ARGV[#ARGV])";

        // 准备 LUA 脚本中的 KEYS 参数，这里只需要一个键，即优惠券模板缓存键
        List<String> keys = Collections.singletonList(couponTemplateCacheKey);

        // 准备 LUA 脚本中的 ARGV 参数，包含优惠券模板的各个字段及其对应值
        List<String> args = new ArrayList<>(actualCacheTargetMap.size() * 2 + 1);
        actualCacheTargetMap.forEach((key, value) -> {
            args.add(key);
            args.add(value);
        });

        // 将优惠券活动的过期时间转换为秒级别的 Unix 时间戳，并添加到 ARGV 参数的末尾
        args.add(String.valueOf(couponTemplateDO.getValidEndTime().getTime() / 1000));

        // 执行 LUA 脚本，使用 stringRedisTemplate 进行 Redis 操作
        // 这里使用 DefaultRedisScript 来定义 LUA 脚本和预期返回值类型
        // keys 和 args 分别提供 LUA 脚本所需的 KEYS 和 ARGV 参数
        stringRedisTemplate.execute(
                new DefaultRedisScript<>(luaScript, Long.class),
                keys,
                args.toArray()
        );

        //发送延时消息，优惠券活动到期修改优惠券模板状态
        CouponTemplateDelayEvent templateDelayEvent = CouponTemplateDelayEvent.builder()
                .shopNumber(UserContext.getShopNumber())
                .couponTemplateId(couponTemplateDO.getId())
                .delayTime(couponTemplateDO.getValidEndTime().getTime()) //
                .build();
        couponTemplateDelayExecuteStatusProducer.sendMessage(templateDelayEvent);

        //添加优惠券模板ID到布隆过滤器
        couponTemplateQueryBloomFilter.add(String.valueOf(couponTemplateDO.getId()));
    }

    /**
     * 优惠券模板分页查询
     *
     * @param requestParam 分页查询请求参数，包含查询条件和分页信息
     * @return 分页查询响应对象，包含优惠券模板信息列表和分页数据
     */
    @Override
    public IPage<CouponTemplatePageQueryRespDTO> pageQueryCouponTemplate(CouponTemplatePageQueryReqDTO requestParam) {
        // 构建分页查询模板 LambdaQueryWrapper
        // 查询条件包括店铺编号、模板名称、商品信息、类型和目标
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                // 等价于 SQL 中的 WHERE shop_number = ?
                .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber())
                // 当模板名称不为空时，进行模糊查询
                .like(StrUtil.isNotBlank(requestParam.getName()), CouponTemplateDO::getName, requestParam.getName())
                // 当商品信息不为空时，进行模糊查询
                .like(StrUtil.isNotBlank(requestParam.getGoods()), CouponTemplateDO::getGoods, requestParam.getGoods())
                // 当类型不为空时，进行精确查询
                .eq(Objects.nonNull(requestParam.getType()), CouponTemplateDO::getType, requestParam.getType())
                // 当目标不为空时，进行精确查询
                .eq(Objects.nonNull(requestParam.getTarget()), CouponTemplateDO::getTarget, requestParam.getTarget());

        requestParam.setCurrent(requestParam.getCurrentPageNumber());
        requestParam.setSize(requestParam.getCurrentPageSize());

        // 使用 MyBatis-Plus 进行分页查询，获取优惠券模板信息
        IPage<CouponTemplateDO> selectPage = couponTemplateMapper.selectPage(requestParam, queryWrapper);

        // 将查询结果转换为分页查询响应对象，便于返回给客户端
        return selectPage.convert(each -> BeanUtil.toBean(each, CouponTemplatePageQueryRespDTO.class));
    }


    @Override
    public CouponTemplateQueryRespDTO findCouponTemplateById(String couponTemplateId) {
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber())
                .eq(CouponTemplateDO::getId, couponTemplateId);

        log.error("currentTime1.3:" + System.currentTimeMillis());
        CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(queryWrapper);
        log.error("currentTime1.4:" + System.currentTimeMillis());
        return BeanUtil.toBean(couponTemplateDO, CouponTemplateQueryRespDTO.class);
    }

    @LogRecord(
            success = "结束优惠券",
            type = "CouponTemplate",
            bizNo = "{{#couponTemplateId}}"
    )
    @Override
    public void terminateCouponTemplate(String couponTemplateId) {
        // 验证是否存在数据横向越权
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber())
                .eq(CouponTemplateDO::getId, couponTemplateId);
        CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(queryWrapper);
        if (couponTemplateDO == null) {
            // 一旦查询优惠券不存在，基本可判定横向越权，可上报该异常行为，次数多了后执行封号等处理
            throw new ClientException("优惠券模板异常，请检查操作是否正确...");
        }

        // 验证优惠券模板是否正常
        if (ObjectUtil.notEqual(couponTemplateDO.getStatus(), CouponTemplateStatusEnum.ACTIVE.getStatus())) {
            throw new ClientException("优惠券模板已结束");
        }

        // 记录优惠券模板修改前数据
        LogRecordContext.putVariable("originalData", JSON.toJSONString(couponTemplateDO));

        // 修改优惠券模板为结束状态
        CouponTemplateDO updateCouponTemplateDO = CouponTemplateDO.builder()
                .status(CouponTemplateStatusEnum.ENDED.getStatus())
                .build();
        Wrapper<CouponTemplateDO> updateWrapper = Wrappers.lambdaUpdate(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getId, couponTemplateDO.getId())
                .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber());
        couponTemplateMapper.update(updateCouponTemplateDO, updateWrapper);

        // 修改优惠券模板缓存状态为结束状态
        String couponTemplateCacheKey = String.format(MerchantAdminRedisConstant.COUPON_TEMPLATE_KEY, couponTemplateId);
        stringRedisTemplate.opsForHash().put(couponTemplateCacheKey, "status", String.valueOf(CouponTemplateStatusEnum.ENDED.getStatus()));
    }

    @LogRecord(
            success = "增加发行量：{{#requestParam.number}}",
            type = "CouponTemplate",
            bizNo = "{{#requestParam.couponTemplateId}}"
    )
    @Override
    public void increaseNumberCouponTemplate(CouponTemplateNumberReqDTO requestParam) {
        // 验证是否存在数据横向越权
        LambdaQueryWrapper<CouponTemplateDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateDO.class)
                .eq(CouponTemplateDO::getShopNumber, UserContext.getShopNumber())
                .eq(CouponTemplateDO::getId, requestParam.getCouponTemplateId());
        CouponTemplateDO couponTemplateDO = couponTemplateMapper.selectOne(queryWrapper);
        if (couponTemplateDO == null) {
            // 一旦查询优惠券不存在，基本可判定横向越权，可上报该异常行为，次数多了后执行封号等处理
            throw new ClientException("优惠券模板异常，请检查操作是否正确...");
        }

        // 验证优惠券模板是否正常
        if (ObjectUtil.notEqual(couponTemplateDO.getStatus(), CouponTemplateStatusEnum.ACTIVE.getStatus())) {
            throw new ClientException("优惠券模板已结束");
        }

        // 记录优惠券模板修改前数据
        LogRecordContext.putVariable("originalData", JSON.toJSONString(couponTemplateDO));

        // 设置数据库优惠券模板增加库存发行量
        int increased = couponTemplateMapper.increaseNumberCouponTemplate(UserContext.getShopNumber(), requestParam.getCouponTemplateId(), requestParam.getNumber());
        if (!SqlHelper.retBool(increased)) {
            throw new ServiceException("优惠券模板增加发行量失败");
        }

        // 增加优惠券模板缓存库存发行量
        String couponTemplateCacheKey = String.format(MerchantAdminRedisConstant.COUPON_TEMPLATE_KEY, requestParam.getCouponTemplateId());
        stringRedisTemplate.opsForHash().increment(couponTemplateCacheKey, "stock", requestParam.getNumber());
    }
}
